#
# Copyright 2009 Red Hat, Inc. and/or its affiliates.
#
# Licensed to you under the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.  See the files README and
# LICENSE_GPL_v2 which accompany this distribution.
#
"""
Cache module provides general purpose (more or less) cache infrastructure
for keeping storage related data that is expensive to harvest, but needed often
"""

import time
import logging
import threading

import cache
import misc
import multipath
import lvm

class StorageDomainCache:
    """
    Storage Domain List keeps track of all the storage domains accessible by the
    current system.
        maxage - time interval at which list content must be considered stale
                 and should be expired

        -1 = never expire
         0 = always expire (effectively no cache at all)
         n = maximum age of cache in seconds
    """

    log = logging.getLogger('irs')
    def __init__(self, storage_repo):
        self.__cache = cache.Cache()
        self.__timestamp = 0 # Cache is cold
        self.storage_repo = storage_repo
        self.storageStale = True


    def invalidateStorage(self):
        self.storageStale = True
        self.invalidate()


    def refreshStorage(self):
        multipath.rescan()
        lvm.updateLvmConf()
        self.storageStale = False
        self.invalidate()


    def refreshExpired(self):
        """
        Check whether the list content is expired
        """
        if self.__timestamp == 0:
            self.log.debug("Cache is cold-expired")
            self._refreshDomains()
            return True

        return False


    def invalidate(self):
        """
        Invalidate the cache data (mark it stale/expired),
        do not, however, actually purge any cached data
        """
        #
        # We do not keep the separate "expired" or "stale" flag,
        # since our isExpire() method checks __timestamp == 0 first thing.
        # If that is the case the cache is considered to be cold.
        # The eventual refresh(), however, will take every precaution
        # not to destroy any existing objects in the cache.
        # It rather makes sure that everything is up to date.
        #
        # That is exactly the effect we are aiming at.
        #
        self.__timestamp = 0

    def flush_deprecated(self):
        """
        Flush the storage domain list
        """
        self.__cache.flush()
        self.__timestamp = 0

    def addDomain(self, domain):
        """
        """
        self.__cache.add(domain.sdUUID, domain)

    def remDomain(self, domain):
        """
        """
        # That may seem awkward
        self.__cache.remove(domain.sdUUID)

    def lookup(self, domainid):
        # In several situations we should refresh the cache before the lookup
        # First, if cache was expired
        # Second, to prevent cache miss when VDC got out of sync
        # For example, VDC gets out of sync if it try to perform refresh when
        # one of domains lost its underlying storage device.
        # In this case we will drop this domain from the cache
        # and we will not be able to add it again if its underlying storage device will come back
        if self.refreshExpired():
            return self.__cache.lookup(domainid)

        dom = self.__cache.lookup(domainid)
        if not dom:
            self._refreshDomains()

        return self.__cache.lookup(domainid)

    def getall(self):
        self.refreshExpired()
        return self.__cache.getvalues()

    def getUUIDs(self):
        self.refreshExpired()
        return self.__cache.getkeys()

    def refreshDomainsMetadata(self):
        threads = []
        for d in self.getall():
            t = threading.Thread(target=d.Metadata, args = (False,))
            threads.append(t)
            t.start()

        for t in threads:
            t.join()


    def refresh(self):
        self.refreshStorage()
        self._refreshDomains()


    @misc.samplingmethod
    def _refreshDomains(self):
        """
        Refresh the domains list
        """
        import blockSD
        import fileSD

        # FIXME: refresh should run under resource lock
        if self.storageStale:
            self.refreshStorage()

        # Get the list of all the domains that are visible *now*
        newdoms = (blockSD.getBlockStorageDomainList() +
            fileSD.getFileStorageDomainList())
        newids = [i.sdUUID for i in newdoms]

        # Get the list of cached domain's UUIDs
        cached = self.__cache.getkeys()

        # Sanity check
        idsdict = dict(zip(newids, [0]*len(newids)))
        for nid in newids:
            idsdict[nid] += 1

        for nid in idsdict:
            if idsdict[nid] > 1:
                if nid not in cached:
                # If we find new domain several time, ignore it
                    self.log.error("Clashed domains. New domain %s has been seen several times and will be ignored.", nid)
                    for i in range(idsdict[nid]):
                        newids.remove(nid)
                else:
                # If we find existing domain several time, ignore its new instance
                    self.log.error("Clashed domains. Existing domain %s has been seen several times and its new instance will be ignored.", nid)
                    for i in range(idsdict[nid] - 1):
                        newids.remove(nid)

        # Generate a list of domains to add and to delete
        toadd = [i for i in newids if i not in cached]
        todel = [i for i in cached if i not in newids]

        # Delete all the unavailable domains
        for d in todel:
            try:
                dom = self.__cache.lookup(d)
                self.remDomain(dom)
                dom.invalidate()
            except Exception, e:
                misc.logException(e)

        # Add all the new domains
        for d in toadd:
            for dom in newdoms:
                if d == dom.sdUUID:
                    try:
                        self.addDomain(dom)
                        break
                    except Exception, e:
                        misc.logException(e)

        self.__timestamp = time.time()

        self.refreshDomainsMetadata()


